docs(api): the 409 unique-constraint entry is UNIQUE_VIOLATION on the wire, not DUPLICATE_RECORD - #15750
Conversation
… wire (#15631) `content/docs/api/error-catalog.mdx` catalogued `DUPLICATE_RECORD` under `## Conflict Errors (409)` and in the HTTP Status Quick Reference. Per the maintainer ruling on #14723 (2026-09-03) a unique-constraint refusal has ONE wire spelling on every route, `UNIQUE_VIOLATION`; `DuplicateRecordError.code` stays `DUPLICATE_RECORD` in-process only, translated at the REST door (`packages/rest/src/error-response.ts`, the `DuplicateRecordError` arm of `structuredCodeAnswer`). A client branching on the catalogued constant never matched. The entry is renamed to the wire code with a one-sentence cross-reference to the in-process spelling, and the quick-reference row follows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Brings in #15762 (#15749), the error-status-conformance deriver fix that stops counting a door-translated in-process class as a wire producer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
|
ACCEPT — PR #15750 (head Per the #14723 ruling: the 409 entry is renamed Measured by this seat: the deriver fix Flip + arm when both required jobs report success on Generated by Claude Code |
|
ACCEPT WITHDRAWN — CI red is this PR's, and the fix is a contract decision, not a rework. Both required jobs are green ( Generated by Claude Code |
…632-error-docs-wire-shape
…ire face
The guard asserted that the error-catalog page's `### `CODE`` headings and the
`StandardErrorCode` enum agree in both directions. That premise broke twice:
- A translated code is not on the wire. `DuplicateRecordError` declares
`code = 'DUPLICATE_RECORD'` and the REST door translates the envelope at the
boundary, so every route answers `UNIQUE_VIOLATION` (#14723). Demanding a
`DUPLICATE_RECORD` heading on a page that documents the wire demands the page
publish a code no client can receive.
- A ledger code IS on the wire. `INVALID_REQUEST` is not an enum member, yet the
catalog publishes two `/meta` entries for it. "Every heading is an enum member"
should have failed on them and did not: the old regex was anchored and both
headings carry a descriptive suffix. They passed by accident.
Per the maintainer ruling on #15631, the page catalogs the WIRE FACE and the guard
compares against that, in both directions. An enum member the translation census
marks as translated is exempt from "must have a heading" — because it is not a wire
code, not by a special case — and must instead be named by the cross-reference
sentence under its wire code's entry, which the guard now asserts.
The wire face and the translation set come from the one place that already derives
them. `check-error-status-conformance.mjs` grows `deriveWireFace()` — the corpus
walk, the runtime side, the doc side, the reconciled vocabulary and that vocabulary
minus the door's translations — and `main()` becomes a consumer of it rather than an
inlining of it. A second hand-written list of translated codes here would be exactly
the copy that file's header argues against. Matching headings by that module's
`ENTRY_HEADING_SHAPES` rather than by a regex of the test's own is the same move, and
is what closes the `INVALID_REQUEST` suffix accident.
The advertised count on the page becomes what the page now promises: 51 codes
reachable on the wire, not 50 enum members.
`scripts/check-error-status-conformance.d.mts` declares the one supported export for
the TS consumer, per the `check-declaration-mirrors` convention.
Gate output and `--self-test` are byte-identical before and after the refactor.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TezFG8ZMrNH6n5VTNpPpdH
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Dev report — the ADR-0112 D7 guard, re-pointed at the wire faceResumed the interrupted patch round on this branch (previous seat cut off mid-edit by a rate limit). The uncommitted edit found on disk
That refactor is behaviour-preserving and it is measured: the gate's full stdout is byte-identical The diff
The ruling, item by item
Red first, then greenThe old guard, re-checked out onto the merged head New guard, same tree: AblationsRun on the committed tree. Each mutation was proved to have reached disk by counting the exact token
Each fires one assertion and leaves the other four green, so neither is a blanket red that any The guard's failure text carries the remedy, not just the disagreement — e.g. A fifth assertion exists purely as a positive control: the derivation must be non-empty and its Gates
Lint, narrowed — and the narrowing measured, not asserted: population 6,275 files, computed CI on
|
| Required context | Conclusion |
|---|---|
Lint & Repo Gates |
success |
TypeScript Type Check |
success |
Test Core |
success |
Dogfood Regression Gate |
success |
Build Core |
success |
Temporal Conformance (live PG + MySQL) |
success |
Test Core is the family that carried the withdrawal's red — Test Core (1/6), the shard that ran
error-catalog-docs.test.ts, is green on this head. Check Changeset is skipped, which is
skip-changeset doing its job per ruling item 4.
Flipping ready re-triggered CI on the same head, as it always does; auto-merge holds the PR until
that generation is green, which is the queue's own re-verification and not something this seat waits
on.
Queue action
Non-governed (0 of 4 path(s) hit the register), green, and accepted-pending — so the ordinary
AGENTS.md §7 path applies: ready first, arm second. Both confirmed by timeline events, which is
the effect reading rather than the response:
2026-09-07T11:45:57Z ready_for_review
2026-09-07T11:46:12Z auto_merge_enabled
Arming stores method: MERGE no matter what is requested; that is immaterial here because main's
merge-queue rule carries merge_method: SQUASH and the queue performs the merge — one squash commit
per PR. ⛔ Not re-armed and will not be: the auto_merge field is unstable within the minute after
arming, and a re-arm kicks the entry out of the queue and re-sorts it.
Out of scope, filed
- spec's declared test-input radius omits
packages/**/*.tsxwhile its tree-walking tests read.tsx— and the glob that would cover it is forbidden by a dispatch-gates self-test pin #16593 —@objectstack/spec's declared cross-package test-input radius omits
packages/**/*.tsxwhile two of its tests (including this guard, now) walk the wholepackages/
tree reading.tsx.check:cross-package-test-inputsis green and correctly so: both walks
descend on a loop variable, so no path NAME resolves and the roster gains no entry. The obvious
widening is forbidden — the declaration's own comment records thatpackages/**/*.tsxmatches
realtime-hooks.test.tsxand would red a dispatch-gates self-test pin. Adjacent class card:
[finding]check:cross-package-test-inputscannot red an undeclared tree-scoped walk inside an already-declared package — the seed escape depth is computed but never asserted #15565. Filed unassigned, not fixed here: the remedy touches a declaration another pin holds.
Not measured
- The 13
dist/-reading gates listed above (unbuilt tree; no path in this diff can reach adist/). - The repo-wide
pnpm lintsweep; the narrowing over 3 of 6,275 files is measured and stated above. check:pm-dispatch-gates— a ~1,400-case self-test of a script this diff does not touch, derived
only because the change set containsscripts/paths. Still running at report time (1,400+ cases and counting under box contention); it is covered byLint & Repo Gates, which is green on this head.
Generated by Claude Code
Fixes #15631
Card #15632 is measured but NOT changed on this branch — its premise did not survive the measurement it asked for, and it stays open for grading. Details under "Measurement 2" below.
No longer docs-only — the second round re-points the ADR-0112 D7 guard per the #15631 ruling; see "The guard, re-pointed (ruled)" below. The first round's docs change was verified at
8888b3862, which mergesorigin/main(no rebase) to pick up #15762 — the deriver fix for #15749 filed from this branch.e52bb6a44is an ancestor of HEAD (git merge-base --is-ancestorexit 0, self-certifying); the diff againstorigin/mainis still the one file.What changed
content/docs/api/error-catalog.mdxcatalogued the 409 unique-constraint refusal under the engine's in-process spelling. Per the maintainer ruling on #14723 (2026-09-03) a unique-constraint refusal has ONE wire spelling on every route,UNIQUE_VIOLATION;DuplicateRecordError.codestaysDUPLICATE_RECORDin-process only, translated at the REST door. A client branching on the catalogued constant never matched.## Conflict Errors (409)entry is renamed toUNIQUE_VIOLATION, Cause / Fix / Retry kept, with one cross-reference sentence naming the in-process spelling and where the translation happenspackages/restis the judge here, not the patient — nothing underpackages/**is touched.Per-example reconciliation
UNIQUE_VIOLATIONpackages/rest/src/error-response.ts:993— theDuplicateRecordErrorarm ofstructuredCodeAnsweranswersstatus: 409,code: 'UNIQUE_VIOLATION'; pinned key-for-key bypackages/rest/src/rest-duplicate-record-arm.test.tsUNIQUE_VIOLATIONhad 0 occurrences### UNIQUE_VIOLATIONentry + quick-reference rowDUPLICATE_RECORDpackages/objectql/src/duplicate-record-error.ts—readonly code = DUPLICATE_RECORD_CODE, thrown in-process; census ofcode: 'DUPLICATE_RECORD'acrosspackages/**finds no wire producer, only docblocks and that throwLive control for the "0 occurrences" reading: in the same run
UNIQUE_VIOLATIONmatched in six other files undercontent/docs/(http-protocol.mdx,error-handling.mdx,contract.mdx,error-code-ledger.mdx,drivers.mdx,releases/v17.mdx) — the zero was the page, not the instrument.Measurement 1 — does any producer emit
success/details/requestId/timestamp?Asked of the judge first, then repo-wide.
structuredCodeAnswer/resolveErrorResponse: NO. Zero occurrences of all four in any emitted body inpackages/rest/src/error-response.ts(the four hits in that file are prose in docblocks). Positive control:code:matches 37 times in the same file, so the grep was not blind.success: YES, elsewhere.sendOk/sendErrorinpackages/types/src/response-envelope.ts:83and:251write{ success: true, data }and{ success: false, error: { code, message, ...extra } }. 95 non-test call sites acrosspackages/rest,packages/services/*andplugins/*.details: YES, elsewhere. Carried insendError'sextra, typed as aPickofApiError's own optionals. Live example:packages/services/service-settings/src/settings-routes.ts:74sends{ details: { namespace: err.namespace } }.requestId: DECLARED, NOT EMITTED. It is onApiErrorSchemaand accepted bysendError'sextra, but no call site supplies one — the only read isresponse-envelope.ts:227, feedinglogServerFault. Nothing puts it on a response body today.timestamp: NOT EMITTED on any error body. Zero occurrences in botherror-response.tsandresponse-envelope.ts; positive control,successmatches 8 times in the latter. It exists asEnhancedApiErrorSchema.timestampand asmeta.timestamponBaseResponseSchema/ErrorResponseSchema, both unset by any producer found.Measurement 2 — does a real consumer read the nested shape? YES, and it stops the #15632 rewrite
This was the dispatch's stop condition, and it is met.
packages/client/src/index.tsreads BOTH envelopes' declared spots, deliberately and by name::6017—asSemanticCode(errorBody?.code) ?? asSemanticCode(errorBody?.error?.code):6020—Array.isArray(errorBody?.error?.details?.fields) ? errorBody.error.details.fields:6035—error.category = errorBody?.error?.category:6043—error.details = errorBody?.details ?? errorBody?.error?.details ?? errorBodyThe comment above them states the reason verbatim: "The two reads below are the two LIVE envelopes' declared spots, not a fallback chain — the flat shape's retirement belongs to the envelope-convergence line (#3843)." It names both dialects explicitly:
@objectstack/rest, flatandruntime dispatcher, wrapped.packages/client/src/shares-envelope-compat.test.tspins that dual read.So the nested envelope is not aspirational. It is declared in the spec (
BaseResponseSchema, andenvelopeViolationsREQUIRES the nested form for a failure body: "a failure body must carryerrorwith a stringcodeandmessage— the nested form"), it has one shared writer, ~95 producer call sites, a gate ratchet (check:route-envelope), and a real SDK consumer.This falsifies #15632's premise. The envelope published on
error-handling.mdxdoes not match "no route's response body" — it matches the whole dispatcher and services route family. The strongest single piece of evidence is on the OTHER page in this dispatch:content/docs/api/error-catalog.mdx:536and:587already publish two nested examples, forGET /api/v1/meta/viewesandPUT /api/v1/meta/fieldz, and those examples are CORRECT for those routes.Rewriting
error-handling.mdxpage-wide to the flat body would therefore have replaced one wrong claim with another. No bytes of that page are touched here.What #15632 actually reports, restated from the measurement: the platform has TWO live error envelopes on different route families, and
error-handling.mdxpublishes one of them under the universally-quantified heading "Every error follows this structure". That is a contract-and-docs-architecture question, not a docs typo — which is why it comes back for grading rather than as a rewrite.The guard, re-pointed (ruled)
The
ACCEPTabove was withdrawn becauseTest Core (1/6)went red inpackages/spec/src/api/error-catalog-docs.test.ts— the ADR-0112 D7 guard — on the docs changethis PR makes. The director ruled on #15631 (comment 5568179518, 2026-09-07) that the guard's
premise, not the page, is what is wrong:
What the diff does
scripts/check-error-status-conformance.mjs— the corpus walk, the runtime side, the doc side andthe reconciled vocabulary move into one exported
deriveWireFace(), andmain()becomes aconsumer of it rather than an inlining of it. The new member is
wireCodes: the reconciledvocabulary minus the codes a door translates away. That subtraction is what makes items 1 and 2 one
rule instead of two — the translated member is exempt from "must have a heading" because it is not
a wire code, not by a special case written beside it.
The refactor is behaviour-preserving, and that is measured rather than asserted: the gate's full
stdout is byte-identical before and after (
diffover the two captured runs reports nodifference), and
--self-teststill reports58 cases pass.scripts/check-error-status-conformance.d.mts(new) declares that one export for the TypeScriptconsumer, per the
check-declaration-mirrorsconvention that already coversjs-comment-mask.d.mtsandcheck-regen-pending.d.mts. Deliberately one export: the moduleexports two dozen internals for its own
--self-test, and declaring those would invite the guard tore-assemble the derivation itself — the second copy item 3 forbids.
node scripts/check-declaration-mirrors.mjs— EXIT=0, and it now reads 9 declaration pairs including thisone.
packages/spec/src/api/error-catalog-docs.test.ts— the guard now importsderiveWireFaceandasserts five things:
StandardErrorCode.optionswireCodes.lengthHeadings are matched via the module's
catalogEntries— i.e. byENTRY_HEADING_SHAPES, the samerule the conformance gate uses — rather than by a regex of the test's own. That is what closes the
INVALID_REQUESTaccident named in item 3: the old guard's/^### \CODE`$/was anchored, and the page's two/metaentries carry a descriptive suffix, so they were never read. An unread heading is an unchecked heading. They are read now, and they pass becauseINVALID_REQUESTgenuinely *is* a wire code — a ledger code the page publishes a400` for.content/docs/api/error-catalog.mdx— the advertised count now states what the page promises:51 error codes reachable on the wire, not 50 enum members, with a short paragraph saying that the
catalog documents the wire face and how a translated code appears on it. The
UNIQUE_VIOLATIONentry and its cross-reference sentence, accepted on 2026-09-05, are unchanged.
The numbers, derived
52 − 1 translated = 51 wire codes. The catalog's 52 read entries collapse to 51 distinct
codes (
INVALID_REQUESThas two entries). Both directions are empty sets: no heading that is nota wire code, no wire code without a heading.
Red first, then green
The old guard is red on the merged head
0dd0dc369with exactly the two failures the withdrawal(comment 5550487210) quoted from the
Test Core (1/6)job log — reproduced locally by checking theold test file and the old page back out of that commit:
The new guard on the same tree:
Test Files 1 passed (1),Tests 5 passed (5), EXIT=0.Ablations
Both mutations the ruling names, each run on the committed tree, each proved to have reached disk
before the guard ran, and each restore proved by blob hash against
HEADplus an emptygit diff HEAD— not by a restore command's exit code.### \DUPLICATE_RECORD`` heading added1 failed | 4 passedDUPLICATE_RECORDoccurrences 1 → 01 failed | 4 passedEach ablation fires one assertion and leaves the other four passing, so neither is a blanket red
that would have shown up whatever broke. Both restores: both blobs equal HEAD, git diff HEAD empty.
Gate verdicts — this round (head
3e66cce22)node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstackderives109 commands over the 4-path change set (up from 39 when this branch was one docs file). All 109
were run, exit code captured before any pipe (
cmd > log 2>&1; EXIT=$?).95 green. Named among them:
check:error-status-conformance(EXIT=0, and--self-test58 cases pass),check:nul-bytes,check:declaration-mirrors(+--self-test),check:cross-package-test-inputs,check:test-source-alias,check:type-check-coverage,check:engine-double-contract,check:page-declaration-shape,check:doc-anchors,check:doc-authoring,check:docs-single-h1,check:scripts-symbol-anchors,check:self-test-wired,check:comment-mask-adoption.13 NOT MEASURED — every one of them an unbuilt-tree prerequisite, none a finding. Seven exit 3
saying so in those words (
check:dts-closure,check:sourcemap-no-sources-content,check:type-check-debt,check:dual-build-cjs-loads,check:docs-transcript-drift, and the two@objectstack/lintdoc gates); six spell the same refusal with exit 1 and the textpackages/spec/dist holds no .d.ts declarations — the package is not built(check:api-surface,check:browser-reachable-entries,check:dual-source-exports,check:entry-nameability,check:exported-any,check:skill-examples).Not built locally, and deliberately: all thirteen read
dist/, and no file in this diff can reachany
dist/. The only path here underpackages/iserror-catalog-docs.test.ts, andpackages/spec/tsconfig.jsoncarries"exclude": ["node_modules", "dist", "**/*.test.ts"]— a testfile is not build input. The other three paths are two root
scripts/files and a docs page. CIbuilds the closure and measures all thirteen.
check:pm-dispatch-gatesgradesscripts/pm/dispatch-gates.mjs, which this diff does not touch; itis derived only because the change set contains
scripts/paths.Lint, narrowed — and the narrowing measured
ESLint#isPathIgnoredovergit ls-files, lintable extensions only), not from an estimate.--format jsonoutput's own length, not from thecommand line.
eslint --no-inline-configover the three lintable changed paths: 0 errors, 0warnings, EXIT=0. (The fourth changed path is
.mdx.)eslint.config.mjsstates and has measured that this repo "never enables type-awarelinting (no
parserOptions.project, no typed@typescript-eslintrules) for ANY file, test ornot" (its own header, around
:328). With no cross-file type information in play, nothing in thisdiff can move the verdict on a file it does not touch — so the three files are the whole of what
this diff put at risk. The repo-wide sweep is CI's.
node scripts/pm/check-governed-merges.mjs --testover the final four-path file list:0 of 4 path(s) hit the register— NOT governed, ordinary queue landing applies.skip-changesetholds per ruling item 4 and is already on the PR.
Gate verdicts — the first round (docs-only head
8888b3862)All 39 commands derived by
node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstackwere re-derived and re-run on the merged head8888b3862, exit code captured before any pipe. The derived command list is byte-identical to the pre-merge derivation, and the change set is still the single pathcontent/docs/api/error-catalog.mdx(merge basee52bb6a44).39 of 39 green. The
check:error-status-conformancered is gone. No command reported exit 3 / PREREQUISITE NOT MET on this run;check:skill-examples, which refused on an unbuiltpackages/client-react/distbefore the merge, is green here because the dependency closures were built first.node scripts/check-error-status-conformance.mjs— EXIT=0:--self-test— EXIT=0,58 cases pass.pnpm check:nul-bytes— EXIT=0,scanned 7618 text file(s) ... no raw ASCII control bytes.DUPLICATE_RECORDis no longer counted as a wire producer: the deriver now reports it undertranslatedand names the door that performs the translation. That is exactly the distinction this branch's docs change asserts, so the gate and the page now agree instead of the gate requiring the page to publish a status for a code that never crosses HTTP.For the record, since it is what #15749 was filed on: before the merge this same gate read
✗ DUPLICATE_RECORD: the runtime can emit HTTP 409, and no doc publishes that status for it (documented: nothing).and✗ check:error-status-conformance — 1 finding(s).on this branch, against exit 0 on the then-currentorigin/mainc2a336ca2— i.e. the gate was green only because the catalog published the falsehood this PR removes. #15762 fixed the deriver rather than the docs bending to it, and the workaround weighed in the earlier revision of this section (a bare### DUPLICATE_RECORDheading absolved through the weakercoveredgrade) is moot and was never taken.Self-check greps
On
content/docs/api/error-catalog.mdxafter the change:DUPLICATE_RECORDoccurs exactly once, in the cross-reference sentence at:361."success": falseoccurs twice and"error": {twice — both are the/api/v1/meta/*examples at:536and:587, which are correct nested bodies for those routes and are deliberately left alone (see Measurement 2).On
content/docs/protocol/kernel/error-handling.mdx: unchanged on this branch —"success": false17,"error": {22, all still nested, pending the #15632 grading.The dispatch's expected self-check was "zero occurrences on both pages after the rewrite". That expectation belonged to a rewrite that the measurement stopped, so the real counts are reported instead of the expected ones.
NOT MEASURED
check:error-status-conformanceasDuplicateRecordErrordoes. Measured forDUPLICATE_RECORDonly.packages/client, so the census stopped at the SDK rather than being carried through every app.content/docs/references/api/error-code-ledger.mdxcarries the same in-process/wire confusion. It is GENERATED and out of fences; not read for this purpose.check-cross-package-test-inputs.mjs --union-into,check-shard-attestation.mjs --emit,check-test-completeness.mjs. Named by the deriver as NOT MEASURED.🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Second round (guard re-point): https://claude.ai/code/session_01TezFG8ZMrNH6n5VTNpPpdH
Generated by Claude Code